﻿using System;
using System.IO;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using System.Xml.Xsl;
using GerbilLib;

namespace GerbilMigrate
{
    /// <summary>
    /// Verifies a given media items cache against the xml schema to determine if the
    /// cache needs to be updated for a newer version of the schema. If an update is
    /// needed it is automatically executed through the appropriate xsl transform.
    /// </summary>
    public class MediaItemsCache
    {
        /// <summary>
        /// Default file name for media items cache
        /// </summary>
        private static readonly string DefaultFilename = "MediaGerbilMediaItemsCache.xml";
        /// <summary>
        /// Temporary file name for xsl transform operations
        /// </summary>
        private static readonly string TempFilename = "MediaGerbilMediaItemsCache.temp";
        /// <summary>
        /// List of currently available media items cache schemas ordered from newest to oldest
        /// </summary>
        private static readonly string[] Schemas = { "MediaGerbilMediaItemsCache-0.3.0.xsd", "MediaGerbilMediaItemsCache-0.2.0.xsd", "MediaGerbilMediaItemsCache-0.1.0.xsd" };
        /// <summary>
        /// List of currently available media items cache schema transforms ordered from newest to oldest
        /// </summary>
        private static readonly string[] Transforms = { "MediaGerbilMediaItemsCache-0.2.0-0.3.0.xslt" };

        /// <summary>
        /// Current schema reader settings
        /// </summary>
        private static XmlReaderSettings schemaReaderSettings = null;
        /// <summary>
        /// Returns the current schema reader settings or creates a new object from default settings
        /// </summary>
        private static XmlReaderSettings SchemaReaderSettings
        {
            get
            {
                if (null == schemaReaderSettings)
                {
                    schemaReaderSettings = new XmlReaderSettings();
                    schemaReaderSettings.ValidationType = ValidationType.Schema;
                    schemaReaderSettings.ValidationFlags = System.Xml.Schema.XmlSchemaValidationFlags.ReportValidationWarnings;
                    schemaReaderSettings.ValidationEventHandler += new System.Xml.Schema.ValidationEventHandler(settings_ValidationEventHandler);
                }

                return schemaReaderSettings;
            }
        }

        /// <summary>
        /// Validates the default media items cache xml file against the provided schema.
        /// </summary>
        /// <param name="currentSchema">Index of the current schema in the schema list</param>
        /// <returns>True if the xml file validates against the schema; otherwise false</returns>
        private static bool ValidateMediaItemsCache(int currentSchema)
        {
            bool isValidMediaItemsCache = true;

            XmlSchemaSet schemaSet = new XmlSchemaSet();
            schemaSet.Add(null, new XmlTextReader(XmlLoader.GetEmbeddedXmlStream(Schemas[currentSchema])));
            SchemaReaderSettings.Schemas = schemaSet;
            XmlReader reader = null;
            try
            {
                reader = XmlReader.Create(DefaultFilename, SchemaReaderSettings);
                while (reader.Read()) ;
                Logger.LogMessage(string.Format("Media Items Cache successfully validated against schema {0}!", Schemas[currentSchema]));
            }
            catch (Exception ex)
            {
                Logger.LogMessage(string.Format("Media Items Cache Validation Error: {0}", ex.Message), LoggingLevel.Debug);
                isValidMediaItemsCache = false;
            }
            finally
            {
                if (reader != null) reader.Close();
            }

            return isValidMediaItemsCache;
        }

        /// <summary>
        /// Event handler for validation events that are fired when validation fails.
        /// </summary>
        /// <param name="sender">Sender object</param>
        /// <param name="e">Validation Event Arguments</param>
        static void settings_ValidationEventHandler(object sender, System.Xml.Schema.ValidationEventArgs e)
        {
            throw new Exception(e.Message, e.Exception);
        }

        /// <summary>
        /// Migrates the current media items cache xml file to a new schema version
        /// by using the appropriate xsl transform, then loads and returns the cache.
        /// </summary>
        /// <param name="currentSchema">Index of the current schema in the schema list.</param>
        /// <returns>Media items cache</returns>
        private static AsyncBindingList<MediaItem> LoadAndMigrateMediaItemsCache(int currentSchema)
        {
            try
            {
                while (currentSchema-- > 0)
                {
                    // Create the XmlReader object.
                    XmlReader reader = XmlReader.Create(DefaultFilename);
                    XslCompiledTransform transform = new XslCompiledTransform();
                    transform.Load(XmlReader.Create(XmlLoader.GetEmbeddedXmlStream(Transforms[currentSchema])));
                    Logger.LogMessage(string.Format("Updating Media Items Cache using transform {0}", Transforms[currentSchema]), LoggingLevel.Debug);

                    // Create a writer for writing the transformed file.
                    XmlWriter writer = XmlWriter.Create(TempFilename);
                    // Execute the transformation.
                    transform.Transform(reader, writer);
                    reader.Close();
                    writer.Close();

                    // Shuffle files around
                    File.Move(DefaultFilename, DefaultFilename + ".orig." + currentSchema);
                    File.Move(TempFilename, DefaultFilename);
                }
            }
            catch (Exception ex)
            {
                Logger.LogMessage(string.Format("An error occurred during Media Items Cache Update: {0}", ex.Message), LoggingLevel.Debug);
                return null;
            }

            // Preferences should have been migrated to latest schema version by now
            if (ValidateMediaItemsCache(0)) return LoadValidatedMediaItemsCache();
            else return null;
        }

        /// <summary>
        /// Saves the current list of media items to an xml file on the local harddisk.
        /// </summary>
        /// <param name="items">The list of media items.</param>
        public static void SaveMediaItems(AsyncBindingList<MediaItem> items)
        {
            XmlSerializer serializer = new XmlSerializer(typeof(AsyncBindingList<MediaItem>));
            TextWriter writer = new StreamWriter(DefaultFilename);
            serializer.Serialize(writer, items);
            writer.Close();
            Logger.LogMessage("Media Items successfully saved!");
        }

        /// <summary>
        /// Attempts to load and return the media items cache. If the cache does not
        /// validate against the most recent schema version, it will be upgraded first
        /// and then loaded.
        /// </summary>
        /// <returns>Media Items Cache</returns>
        public static AsyncBindingList<MediaItem> LoadMediaItemsCache()
        {
            if (!File.Exists(DefaultFilename))
            {
                Logger.LogMessage("No Media Items Cache found.");
                return null;
            }

            int currentSchema = 0;
            bool isValidMediaItemsCache = false;
            while (!isValidMediaItemsCache && currentSchema < Schemas.Length)
            {
                Logger.LogMessage(string.Format("Trying to validate Media Items Cache against schema {0}!", Schemas[currentSchema]));
                isValidMediaItemsCache = ValidateMediaItemsCache(currentSchema);
                if (!isValidMediaItemsCache) currentSchema++;
            }

            if (currentSchema == 0) return LoadValidatedMediaItemsCache();
            Logger.LogMessage("Media Items Cache will be upgraded to latest schema version before loading...");
            return LoadAndMigrateMediaItemsCache(currentSchema);
        }

        /// <summary>
        /// Loads the list of media items from an xml file on the local harddisk.
        /// </summary>
        /// <returns>The list of media items.</returns>
        public static AsyncBindingList<MediaItem> LoadValidatedMediaItemsCache()
        {
            XmlSerializer serializer = new XmlSerializer(typeof(AsyncBindingList<MediaItem>));
            serializer.UnknownAttribute += new XmlAttributeEventHandler(serializer_UnknownAttribute);
            serializer.UnknownElement += new XmlElementEventHandler(serializer_UnknownElement);
            serializer.UnknownNode += new XmlNodeEventHandler(serializer_UnknownNode);

            AsyncBindingList<MediaItem> items = null;
            try
            {
                XmlReader reader = XmlReader.Create(DefaultFilename);
                items = (AsyncBindingList<MediaItem>)serializer.Deserialize(reader);
                reader.Close();
                Logger.LogMessage(string.Format("Media Items successfully loaded! {0} cached items restored.", items.Count));
            }
            catch (FileNotFoundException)
            {
                Logger.LogMessage("No Media Item cache found!");
            }
            catch (InvalidOperationException)
            {
                Logger.LogMessage("There was a problem loading the Media Item cache, continuing without it!");
            }
            return items;
        }

        /// <summary>
        /// Handles the UnknownNode event of the Xml deserializer.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Xml.Serialization.XmlNodeEventArgs"/> instance containing the event data.</param>
        static void serializer_UnknownNode(object sender, XmlNodeEventArgs e)
        {
            Logger.LogMessage(string.Format("MediaItem: Unknown Node {0} detected during deserialization. Ignoring for now...", e.Name), LoggingLevel.Debug);
        }

        /// <summary>
        /// Handles the UnknownElement event of the Xml deserializer.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Xml.Serialization.XmlElementEventArgs"/> instance containing the event data.</param>
        static void serializer_UnknownElement(object sender, XmlElementEventArgs e)
        {
            Logger.LogMessage(string.Format("MediaItem: Unknown Element detected during deserialization. Ignoring for now...", e.Element.Name), LoggingLevel.Debug);
        }

        /// <summary>
        /// Handles the UnknownAttribute event of the Xml deserializer.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Xml.Serialization.XmlAttributeEventArgs"/> instance containing the event data.</param>
        static void serializer_UnknownAttribute(object sender, XmlAttributeEventArgs e)
        {
            Logger.LogMessage(string.Format("MediaItem: Unknown Attribute detected during deserialization. Ignoring for now...", e.Attr.Name), LoggingLevel.Debug);
        }
    }
}
